Skip to main content
All LiveView data is stored in the socket under the assigns key. The way you work with assigns directly impacts performance, as LiveView uses them for change tracking to minimize data sent over the wire.

Understanding Assigns

Assigns are a map stored in socket.assigns that hold all the data your LiveView needs:
# In your LiveView code
socket.assigns.user
socket.assigns.count
socket.assigns.posts

# In your templates  
@user
@count
@posts
The @ syntax in templates is syntactic sugar for assigns.key. Behind the scenes, @user becomes assigns[:user].

Setting Assigns

assign/2 - Set Multiple Assigns

# With a keyword list
assign(socket, name: "Alice", age: 30)

# With a map
assign(socket, %{name: "Alice", age: 30})

# With a function
assign(socket, fn assigns -> 
  %{full_name: "#{assigns.first_name} #{assigns.last_name}"}
end)

assign/3 - Set a Single Assign

assign(socket, :count, 0)
assign(socket, :user, user)
assign(socket, :loading, false)

Chaining Assigns

All assign functions return an updated socket, enabling chaining:
socket
|> assign(:loading, true)
|> assign(:count, 0)
|> assign(:posts, [])

Updating Assigns

update/3 - Transform an Existing Value

# Increment a counter
update(socket, :count, fn count -> count + 1 end)
update(socket, :count, &(&1 + 1))

# Append to a list
update(socket, :posts, fn posts -> [new_post | posts] end)

# Update a map
update(socket, :user, &Map.put(&1, :verified, true))
update/3 raises a KeyError if the key doesn’t exist. Use assign/3 if the key might not be present.

update/3 with Two-Arity Function

Access other assigns while updating:
update(socket, :total, fn _current, assigns -> 
  Enum.sum(assigns.values)
end)

Lazy Assignment

assign_new/3 - Only Set If Not Present

def mount(_params, _session, socket) do
  socket = 
    socket
    |> assign_new(:count, fn -> 0 end)
    |> assign_new(:current_user, fn -> load_user() end)
  
  {:ok, socket}
end
  • Setting default values
  • Sharing assigns from plugs during disconnected render
  • Sharing assigns between parent and child LiveViews
  • Lazy loading expensive data only when needed

Sharing from Plug Pipeline

# In a plug
def assign_user(conn, _opts) do
  assign(conn, :current_user, get_user(conn))
end

# In LiveView mount
def mount(_params, %{"user_id" => user_id}, socket) do
  socket = assign_new(socket, :current_user, fn ->
    # Only runs if not already set by plug
    Accounts.get_user!(user_id)
  end)
  
  {:ok, socket}
end

Change Tracking

LiveView tracks which assigns have changed and only re-renders affected parts of the template.

How It Works

# Initial render
assign(socket, name: "Alice", age: 30, city: "NYC")

# Later, update only name
assign(socket, :name, "Bob")

# LiveView knows:
# - @name changed → re-render parts using @name
# - @age unchanged → don't re-render parts using @age
# - @city unchanged → don't re-render parts using @city

Nested Field Tracking

Change tracking works with map/struct fields:
<div id={"user-#{@user.id}"}>
  {@user.name}
</div>
If you update:
update(socket, :user, &Map.put(&1, :name, "New Name"))
LiveView knows:
  • @user.name changed → re-render
  • @user.id unchanged → don’t re-render or resend

Common Pitfalls

Variables Disable Tracking

Don’t do this - variables disable change tracking:
<% total = @x + @y %>
<p>{total}</p>
LiveView must re-render this on every render, even if @x and @y haven’t changed.
Do this instead - use assigns:
def render(assigns) do
  assigns = assign(assigns, :total, assigns.x + assigns.y)
  ~H"<p>{@total}</p>"
end
Now LiveView only re-renders when @x or @y actually change.

Don’t Access assigns Directly

Don’t do this:
<% some_value = assigns.x + assigns.y %>
{some_value}
<.component {assigns} />
Direct access to the assigns variable disables change tracking.
Do this instead:
{sum(@x, @y)}
<.component x={@x} y={@y} />

Never Use Map Functions on Assigns

Never modify assigns with Map functions:
def my_component(assigns) do
  assigns = Map.put(assigns, :computed, compute(assigns.value))
  ~H"<p>{@computed}</p>"
end
This breaks change tracking completely!
Always use LiveView’s assign functions:
def my_component(assigns) do
  assigns = assign(assigns, :computed, compute(assigns.value))
  ~H"<p>{@computed}</p>"
end

No Data Loading in Templates

Never load data in templates:
<%= for post <- Blog.list_posts() do %>
  {post.title}
<% end %>
This query runs on every render, and LiveView won’t detect changes!
Load data in mount or handle_ callbacks:*
def mount(_params, _session, socket) do
  {:ok, assign(socket, :posts, Blog.list_posts())}
end

Reserved Assigns

Some assign keys are reserved by LiveView:
  • @flash - Flash messages (use put_flash/3)
  • @uploads - File uploads (use allow_upload/3)
  • @streams - Streamed collections (use stream/4)
  • @socket - The socket struct itself
  • @myself - Component reference (LiveComponent only)
  • @live_action - Current live action from router
Attempting to assign these keys directly will raise an error.

Temporary Assigns

Temporary assigns are reset to their initial value after every render:
def mount(_params, _session, socket) do
  {:ok, socket, temporary_assigns: [posts: []]}
end

def handle_event("load_posts", _params, socket) do
  # Assign new posts
  socket = assign(socket, :posts, Blog.list_posts())
  {:noreply, socket}
  # After render, @posts automatically resets to []
end
  • Sending large collections that you don’t want to keep in memory
  • One-time data that’s only needed for a single render
  • Flash-style messages that should disappear
  • Large file upload metadata
Note: For most collection use cases, prefer stream/4 instead.

Streams

Streams allow you to manage collections without keeping them in memory:
def mount(_params, _session, socket) do
  {:ok, stream(socket, :posts, Blog.list_posts())}
end

def handle_event("add_post", %{"post" => post_params}, socket) do
  post = Blog.create_post!(post_params)
  {:noreply, stream_insert(socket, :posts, post, at: 0)}
end

def handle_event("delete_post", %{"id" => id}, socket) do
  Blog.delete_post!(id)
  {:noreply, stream_delete(socket, :posts, %{id: id})}
end
Streams are ideal for large lists, infinite scroll, and real-time feeds where you don’t need to keep all items in memory.

State Patterns

Loading State Pattern

def mount(_params, _session, socket) do
  socket = 
    socket
    |> assign(:loading, true)
    |> assign(:data, nil)
    |> assign(:error, nil)
  
  if connected?(socket) do
    send(self(), :load_data)
  end
  
  {:ok, socket}
end

def handle_info(:load_data, socket) do
  case fetch_data() do
    {:ok, data} ->
      {:noreply, assign(socket, loading: false, data: data)}
    {:error, error} ->
      {:noreply, assign(socket, loading: false, error: error)}
  end
end
In template:
<div :if={@loading}>Loading...</div>
<div :if={@error}>Error: {@error}</div>
<div :if={@data}>
  <!-- Render data -->
</div>

Form State Pattern

def mount(_params, _session, socket) do
  changeset = Accounts.change_user(%User{})
  {:ok, assign(socket, form: to_form(changeset))}
end

def handle_event("validate", %{"user" => user_params}, socket) do
  changeset = 
    %User{}
    |> Accounts.change_user(user_params)
    |> Map.put(:action, :validate)
  
  {:noreply, assign(socket, form: to_form(changeset))}
end

def handle_event("save", %{"user" => user_params}, socket) do
  case Accounts.create_user(user_params) do
    {:ok, user} ->
      {:noreply, push_navigate(socket, to: ~p"/users/#{user}")}
    {:error, changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end

Pagination State Pattern

def mount(_params, _session, socket) do
  socket = 
    socket
    |> assign(:page, 1)
    |> assign(:per_page, 20)
    |> load_page()
  
  {:ok, socket}
end

def handle_event("next_page", _params, socket) do
  socket = 
    socket
    |> update(:page, &(&1 + 1))
    |> load_page()
  
  {:noreply, socket}
end

defp load_page(socket) do
  %{page: page, per_page: per_page} = socket.assigns
  posts = Blog.list_posts(page: page, per_page: per_page)
  assign(socket, :posts, posts)
end

Tab State Pattern

def handle_params(params, _uri, socket) do
  tab = params["tab"] || "overview"
  {:noreply, assign(socket, :active_tab, tab)}
end

def handle_event("switch_tab", %{"tab" => tab}, socket) do
  {:noreply, push_patch(socket, to: ~p"/dashboard?tab=#{tab}")}
end

Search State Pattern

def mount(_params, _session, socket) do
  socket =
    socket
    |> assign(:search_query, "")
    |> assign(:search_results, [])
  
  {:ok, socket}
end

def handle_event("search", %{"query" => query}, socket) do
  results = Search.search(query)
  
  socket =
    socket
    |> assign(:search_query, query)
    |> assign(:search_results, results)
  
  {:noreply, socket}
end

Async Assigns

For expensive operations, use assign_async/3:
def mount(%{"slug" => slug}, _session, socket) do
  {:ok,
   socket
   |> assign(:slug, slug)
   |> assign_async(:org, fn ->
     {:ok, %{org: Orgs.fetch_by_slug!(slug)}}
   end)}
end
In template:
<div :if={@org.loading}>Loading organization...</div>
<div :if={@org.ok? && @org.result}>
  {@org.result.name}
</div>
Don’t pass the socket to async functions:
# BAD - copies entire socket
assign_async(:org, fn -> fetch_org(socket.assigns.slug) end)

# GOOD - only copy what you need
slug = socket.assigns.slug
assign_async(:org, fn -> fetch_org(slug) end)

Debugging Assigns

Inspect in Templates

<pre>{inspect(@socket.assigns, pretty: true)}</pre>
<pre>{inspect(@user, pretty: true)}</pre>

Inspect in Code

def handle_event("debug", _params, socket) do
  IO.inspect(socket.assigns, label: "Current Assigns")
  {:noreply, socket}
end

Using IEx

def handle_event("breakpoint", _params, socket) do
  require IEx; IEx.pry()
  {:noreply, socket}
end

Best Practices

Only store what you actually need. Large data structures consume memory and slow down change tracking.
# Good
assign(socket, :user_search_results, results)

# Bad
assign(socket, :results, results)
Set default values for all assigns your LiveView uses:
def mount(_params, _session, socket) do
  socket =
    socket
    |> assign(:loading, false)
    |> assign(:error, nil)
    |> assign(:data, [])
  
  {:ok, socket}
end
When modifying existing values, prefer update/3 over reading and assigning:
# Good
update(socket, :count, &(&1 + 1))

# Less clear
assign(socket, :count, socket.assigns.count + 1)

Summary

  • Assigns are stored in socket.assigns and accessed as @key in templates
  • Use assign/2, assign/3, and assign_new/3 to set assigns
  • Use update/3 to transform existing assigns
  • Change tracking automatically optimizes what’s sent to the client
  • Never access the assigns variable directly in templates
  • Never use Map functions to modify assigns
  • Avoid variables in templates (they disable tracking)
  • Load data in callbacks, not in templates
  • Use temporary assigns or streams for large collections
  • Keep assigns minimal for better performance